﻿/*
* Copyright (c) 2010-2012 Tesla Engine Group
* 
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
* 
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
* 
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/


using System;
using Tesla.Direct3D10.Graphics.Implementation;
using Tesla.Graphics;
using Tesla.Math;
using D3D = SlimDX.Direct3D10;
using DXGI = SlimDX.DXGI;

namespace Tesla.Direct3D10.Graphics {
    /// <summary>
    /// Direct3D10 implementation of <see cref="BaseRenderer"/>.
    /// </summary>
    public class D3D10Renderer : BaseRenderer {
        private D3D.Device _graphicsDevice;
        private DXGI.Factory _factory;
        private D3D10GraphicsAdapter _adapter;
        private ResourceTracker _tracker;

        //Cached properties
        private Viewport _viewPort;
        private Rectangle _scissorRectangle;
        private Color _blendFactor;
        private int _blendSampleMask;
        private int _depthStencilReference;

        //Vertex buffer variables
        private VertexBufferBinding[] _currentVertexBuffers;
        private D3D.VertexBufferBinding[] _vertexBindings;
        private int _currentVertexBufferCount;
        private IndexBuffer _currentIndexBuffer;
        private VertexBufferBinding[] _tempBuffer;

        //Render target variables
        private RenderTargetManager _renderTargetManager;
        private RenderTargetBinding[] _tempTarget;

        //Texture collections
        private D3D10TextureCollection _textures;
        private D3D10TextureCollection _vertexTextures;

        //Sampler collections
        private D3D10SamplerStateCollection _samplers;
        private D3D10SamplerStateCollection _vertexSamplers;

        //Needed for storing and creating input layouts that are specific to D3D10/11
        private D3D10EffectPass _currentPass;

        /// <summary>
        /// Internal resource tracker.
        /// </summary>
        internal ResourceTracker Resources {
            get {
                return _tracker;
            }
        }

        /// <summary>
        /// Internal render target manager.
        /// </summary>
        internal RenderTargetManager TargetManager {
            get {
                return _renderTargetManager;
            }
        }

        /// <summary>
        /// D3D10 Graphics Device
        /// </summary>
        internal D3D.Device GraphicsDevice {
            get {
                return _graphicsDevice;
            }
        }

        /// <summary>
        /// DXGI Factory.
        /// </summary>
        internal DXGI.Factory DXGIFactory {
            get {
                return _factory;
            }
        }

        /// <summary>
        /// Gets or sets the viewport which identifies the portion of the render target which we will be rendering onto.
        /// </summary>
        public override Viewport Viewport {
            get {
                return _viewPort;
            }
            set {
                _viewPort = value;
                _graphicsDevice.Rasterizer.SetViewports(D3D10Helper.ToD3DViewport(_viewPort));
            }
        }

        /// <summary>
        /// Gets or sets the rectangle used for scissor testing.
        /// </summary>
        public override Rectangle ScissorRectangle {
            get {
                return _scissorRectangle;
            }
            set {
                _scissorRectangle = value;
                System.Drawing.Rectangle rec = new System.Drawing.Rectangle(_scissorRectangle.X,
                    _scissorRectangle.Y, _scissorRectangle.Width, _scissorRectangle.Height);
                _graphicsDevice.Rasterizer.SetScissorRectangles(rec);
            }
        }

        /// <summary>
        /// Gets or sets the blend factor which is a constant color used for alpha blending. Default is Color.White. This is a "high frequency" render state,
        /// and setting a BlendState to the renderer will override this value.
        /// </summary>
        public override Color BlendFactor {
            get {
                return _blendFactor;
            }
            set {
                _blendFactor = value;

                SlimDX.Color4 bf;
                bf.Red = _blendFactor.R;
                bf.Green = _blendFactor.G;
                bf.Blue = _blendFactor.B;
                bf.Alpha = _blendFactor.A;

                _graphicsDevice.OutputMerger.BlendFactor = bf;
            }
        }

        /// <summary>
        /// Gets or sets the bitmask which defines which samples can be written during multisampling. Default is -1 (0xffffffff). This is a "high frequency" render
        /// state, and setting a BlendState to the renderer will override this value.
        /// </summary>
        public override int MultiSampleMask {
            get {
                return _blendSampleMask;
            }
            set {
                _blendSampleMask = value;
                _graphicsDevice.OutputMerger.BlendSampleMask = _blendSampleMask;
            }
        }

        /// <summary>
        /// Gets or sets the reference value for stencil testing. Default is zero. This is a "high frequency" render state, and setting a BlendState to the
        /// renderer will override this value.
        /// </summary>
        public override int ReferenceStencil {
            get {
                return _depthStencilReference;
            }
            set {
                _depthStencilReference = value;
                _graphicsDevice.OutputMerger.DepthStencilReference = _depthStencilReference;
            }
        }

        /// <summary>
        /// Gets the sampler state collection for setting/getting sampler states used by the pixel shader.
        /// </summary>
        public override ISamplerStateCollection SamplerStates {
            get {
                return _samplers;
            }
        }

        /// <summary>
        /// Gets the sampler state collection for setting/getting sampler states used by the vertex shader.
        /// </summary>
        public override ISamplerStateCollection VertexSamplerStates {
            get {
                return _vertexSamplers;
            }
        }

        /// <summary>
        /// Gets the texture collection for setting/getting textures used by the pixel shader.
        /// </summary>
        public override ITextureCollection Textures {
            get {
                return _textures;
            }
        }

        /// <summary>
        /// Gets the texture collection for setting/getting textures used by the vertex shader.
        /// </summary>
        public override ITextureCollection VertexTextures {
            get {
                return _vertexTextures;
            }
        }

        /// <summary>
        /// Gets the graphics adapter that was used to create the renderer.
        /// </summary>
        public override IGraphicsAdapter Adapter {
            get {
                return _adapter;
            }
        }

        /// <summary>
        /// Creates a new instance of <see cref="D3D10Renderer"/>.
        /// </summary>
        /// <param name="factory">The DXGI factory used to create the adapter for the device.</param>
        /// <param name="graphicsDevice">The graphics device created by the render system.</param>
        /// <param name="adapter">The graphics adapter used to create the device.</param>
        internal D3D10Renderer(DXGI.Factory factory, D3D.Device graphicsDevice, D3D10GraphicsAdapter adapter) {
            _factory = factory;
            _graphicsDevice = graphicsDevice;
            _adapter = adapter;
            CreateHelpers();
        }

        /// <summary>
        /// Releases unmanaged resources and performs other cleanup operations before the
        /// <see cref="D3D10Renderer"/> is reclaimed by garbage collection.
        /// </summary>
        ~D3D10Renderer() {
            Dispose(false);
        }

        private void CreateHelpers() {
            _currentVertexBuffers = new VertexBufferBinding[_adapter.MaximumVertexStreams];
            _vertexBindings = new D3D.VertexBufferBinding[_adapter.MaximumVertexStreams];
            _tempBuffer = new VertexBufferBinding[1];
            _currentVertexBufferCount = 0;

            _textures = new D3D10TextureCollection(this, _adapter.MaximumSamplerStates, false);
            _vertexTextures = new D3D10TextureCollection(this, _adapter.MaximumVertexSamplerStates, true);

            _samplers = new D3D10SamplerStateCollection(this, _adapter.MaximumSamplerStates, false);
            _vertexSamplers = new D3D10SamplerStateCollection(this, _adapter.MaximumVertexSamplerStates, true);

            _renderTargetManager = new RenderTargetManager(this, _adapter.MaximumMultiRenderTargets);
            _tempTarget = new RenderTargetBinding[1];

            _tracker = new ResourceTracker(this);
        }

        /// <summary>
        /// Draws non-indexed geometry.
        /// </summary>
        /// <param name="primType">Type of the primitives to draw</param>
        /// <param name="vertexCount">Number of vertices to draw</param>
        /// <param name="startVertex">Starting index in the vertex buffer at which to read vertices from.</param>
        public override void Draw(PrimitiveType primType, int vertexCount, int startVertex) {
            D3D10Helper.CheckDisposed(_graphicsDevice);
            if(vertexCount <= 0) {
                throw new ArgumentOutOfRangeException("vertexCount", "Vertex count must be greater than zero.");
            }

            //Send any remaining texture/sampler changes to the device.
            FlushStates();

            _graphicsDevice.InputAssembler.SetInputLayout(_currentPass.InputLayoutMap.GetOrCreateLayout(_currentVertexBuffers, _currentVertexBufferCount));
            _graphicsDevice.InputAssembler.SetPrimitiveTopology(D3D10Helper.ToD3DPrimitiveType(primType));
            _graphicsDevice.Draw(vertexCount, startVertex);
        }

        /// <summary>
        /// Draws indexed geometry.
        /// </summary>
        /// <param name="primType">Type of the primitives to draw.</param>
        /// <param name="indexCount">Number of indices to draw.</param>
        /// <param name="startIndex">Starting index in the index buffer at which to read vertex indices from.</param>
        /// <param name="baseVertex">Offset to add to each vertex index in the index buffer.</param>
        public override void DrawIndexed(PrimitiveType primType, int indexCount, int startIndex, int baseVertex) {
            D3D10Helper.CheckDisposed(_graphicsDevice);
            if(indexCount <= 0) {
                throw new ArgumentOutOfRangeException("indexCount", "Index count must be greater than zero.");
            }

            if(_currentIndexBuffer == null) {
                throw new ArgumentNullException("DrawIndexed requires a valid index buffer.");
            } else {
                D3D10Helper.CheckDisposed(_currentIndexBuffer);
            }

            //Send any remaining texture/sampler changes to the device.
            FlushStates();

            _graphicsDevice.InputAssembler.SetInputLayout(_currentPass.InputLayoutMap.GetOrCreateLayout(_currentVertexBuffers, _currentVertexBufferCount));
            _graphicsDevice.InputAssembler.SetPrimitiveTopology(D3D10Helper.ToD3DPrimitiveType(primType));
            _graphicsDevice.DrawIndexed(indexCount, startIndex, baseVertex);
        }

        /// <summary>
        /// Draws indexed, instanced geometry.
        /// </summary>
        /// <param name="primType">Type of the primitives to draw.</param>
        /// <param name="indexCountPerInstance">Number of indices per instance to draw.</param>
        /// <param name="startIndex">Starting index in the index buffer at which to read vertex indices from.</param>
        /// <param name="baseVertex">Offset to add to each vertex index in the index buffer.</param>
        /// <param name="instanceCount">Number of instances to draw.</param>
        public override void DrawIndexedInstanced(PrimitiveType primType, int indexCountPerInstance, int startIndex, int baseVertex, int instanceCount) {
            D3D10Helper.CheckDisposed(_graphicsDevice);
            if(indexCountPerInstance <= 0) {
                throw new ArgumentOutOfRangeException("vertexCount", "Index count must be greater than zero.");
            }
            if(instanceCount <= 0) {
                throw new ArgumentOutOfRangeException("instanceCount", "Number of instances to draw must be greater than zero.");
            }

            if(_currentIndexBuffer == null) {
                throw new ArgumentNullException("DrawIndexedInstanced requires a valid index buffer.");
            } else {
                D3D10Helper.CheckDisposed(_currentIndexBuffer);
            }

            //Send any remaining texture/sampler changes to the device.
            FlushStates();

            _graphicsDevice.InputAssembler.SetInputLayout(_currentPass.InputLayoutMap.GetOrCreateLayout(_currentVertexBuffers, _currentVertexBufferCount));
            _graphicsDevice.InputAssembler.SetPrimitiveTopology(D3D10Helper.ToD3DPrimitiveType(primType));
            _graphicsDevice.DrawIndexedInstanced(indexCountPerInstance, instanceCount, startIndex, startIndex, 0);
        }

        /// <summary>
        /// Clears all bounded render targets.
        /// </summary>
        /// <param name="options">Options specifying which buffer to clear - Color, Depth, and Stencil</param>
        /// <param name="color">Color to clear to</param>
        /// <param name="depth">Depth value to clear to</param>
        /// <param name="stencil">Stencil value to clear to</param>
        public override void Clear(ClearOptions options, Color color, float depth, int stencil) {
            _renderTargetManager.ClearTargets(options, color, depth, stencil);
        }

        /// <summary>
        /// Sets a 2D render target to the first render target slot and unbinds
        /// all currently bound targets.
        /// Setting this to null
        /// unbinds all targets and sets the target to the currently active
        /// swap chain's back buffer.
        /// </summary>
        /// <param name="renderTarget">Render target to bind</param>
        public override void SetRenderTarget(RenderTarget2D renderTarget) {
            if(renderTarget == null) {
                _renderTargetManager.SetRenderTargets(null);
            } else {
                _tempTarget[0] = new RenderTargetBinding(renderTarget);
                _renderTargetManager.SetRenderTargets(_tempTarget);
            }
        }

        /// <summary>
        /// Sets a Cube render target, to the first render target slot
        /// and unbinds all currently bound targets.
        /// Setting this to null unbinds all targets and sets the target to the currently active
        /// swap chain's back buffer.
        /// </summary>
        /// <param name="renderTarget">Render target to bind</param>
        /// <param name="cubeMapFace">Which face of the cube map to bind</param>
        public override void SetRenderTarget(RenderTargetCube renderTarget, CubeMapFace cubeMapFace) {
            if(renderTarget == null) {
                _renderTargetManager.SetRenderTargets(null);
            } else {
                _tempTarget[0] = new RenderTargetBinding(renderTarget, cubeMapFace);
                _renderTargetManager.SetRenderTargets(_tempTarget);
            }
        }

        /// <summary>
        /// Sets the array of render targets to the device and unbinds all currently
        /// bound targets. The first target in the array is bound to the first
        /// slot and so forth. The first render target's depth-stencil buffer is used
        /// for all targets.
        /// </summary>
        /// <param name="renderTargets">Array of targets</param>
        public override void SetRenderTargets(params RenderTargetBinding[] renderTargets) {
            _renderTargetManager.SetRenderTargets(renderTargets);
        }

        /// <summary>
        /// Gets the currently bounded render targets.
        /// </summary>
        /// <returns>
        /// Current render targets
        /// </returns>
        public override RenderTargetBinding[] GetRenderTargets() {
            return _renderTargetManager.CurrentRenderTargets;
        }

        /// <summary>
        /// Sets the vertex buffer to the first input slot onto the device.
        /// </summary>
        /// <param name="vertexBuffer">Vertex buffer to bind</param>
        public override void SetVertexBuffer(VertexBuffer vertexBuffer) {
            if(vertexBuffer != null) {
                _tempBuffer[0] = new VertexBufferBinding(vertexBuffer);
                SetVertexBuffers(_tempBuffer, 1);
            } else {
                SetVertexBuffers(null, 0);
            }
        }

        /// <summary>
        /// Sets the vertex buffer to the first input slot onto the device
        /// </summary>
        /// <param name="vertexBuffer">Vertex buffer to bind</param>
        /// <param name="vertexOffset">Offset from the beginning of the buffer to the first vertex to use (in bytes).</param>
        public override void SetVertexBuffer(VertexBuffer vertexBuffer, int vertexOffset) {
            if(vertexBuffer != null) {
                _tempBuffer[0] = new VertexBufferBinding(vertexBuffer, vertexOffset);
                SetVertexBuffers(_tempBuffer, 1);
            } else {
                SetVertexBuffers(null, 0);
            }
        }

        /// <summary>
        /// Sets an array of vertex buffers starting from the first input slot.
        /// </summary>
        /// <param name="vertexBuffers">Array of VertexBufferBindings, each binding should specify a vertex buffer, a vertex offset, and an instancing frequency.</param>
        public override void SetVertexBuffers(params VertexBufferBinding[] vertexBuffers) {
            if(vertexBuffers != null && vertexBuffers.Length > 0) {
                SetVertexBuffers(vertexBuffers, vertexBuffers.Length);
            } else {
                SetVertexBuffers(null, 0);
            }
        }

        private void SetVertexBuffers(VertexBufferBinding[] vertexBuffers, int vbCount) {
            //Do some quick validation first
            D3D10Helper.CheckDisposed(_graphicsDevice);
            if(vbCount > _adapter.MaximumVertexStreams || vbCount > vertexBuffers.Length) {
                throw new ArgumentOutOfRangeException("vertexBuffers", "Number of vertex buffers to bind to the device exceeds the maximum number of streams.");
            }

            //Clear the currently bound set of buffers
            Array.Clear(_currentVertexBuffers, 0, _currentVertexBufferCount);
            Array.Clear(_vertexBindings, 0, _currentVertexBufferCount);
            _currentVertexBufferCount = 0;

            //Iterate over each buffer, check if it's valid and add it to the bindings array
            for(int i = 0; i < vbCount; i++) {
                VertexBufferBinding binding = vertexBuffers[i];
                VertexBuffer vb = binding.VertexBuffer;

                //Ensure the buffer exists
                D3D10Helper.CheckDisposed(vb);

                _currentVertexBuffers[_currentVertexBufferCount] = binding;

                D3D10VertexBufferImplementation impl = (D3D10VertexBufferImplementation) vb.Implementation;
                _vertexBindings[_currentVertexBufferCount] = new D3D.VertexBufferBinding(impl.D3DVertexBuffer, vb.VertexDeclaration.VertexStride, binding.VertexOffset);
               
                _currentVertexBufferCount++;
            }

            //Set the D3D10 bindings to the device
            if(vbCount == 1) {
                _graphicsDevice.InputAssembler.SetVertexBuffers(0, _vertexBindings[0]);
            } else {
                _graphicsDevice.InputAssembler.SetVertexBuffers(0, _vertexBindings);
            }
        }

        /// <summary>
        /// Gets the currently bounded vertex buffers.
        /// </summary>
        /// <returns>
        /// Current render targets
        /// </returns>
        public override VertexBufferBinding[] GetVertexBuffers() {
            VertexBufferBinding[] buffers = new VertexBufferBinding[_currentVertexBufferCount];
            Array.Copy(_currentVertexBuffers, buffers, _currentVertexBufferCount);
            return buffers;
        }

        /// <summary>
        /// Sets the index buffer.
        /// </summary>
        /// <param name="ib">The ib.</param>
        public override void SetIndexBuffer(IndexBuffer ib) {
            _currentIndexBuffer = ib;
            if(ib != null) {
                D3D10Helper.CheckDisposed(ib);
                D3D10IndexBufferImplementation impl = (D3D10IndexBufferImplementation) ib.Implementation;
                _graphicsDevice.InputAssembler.SetIndexBuffer(impl.D3DIndexBuffer, impl.D3DIndexFormat, 0);
            }
        }

        /// <summary>
        /// Gets the currently bound index buffer.
        /// </summary>
        /// <returns>
        /// Current index buffer
        /// </returns>
        public override IndexBuffer GetIndexBuffer() {
            return _currentIndexBuffer;
        }

        /// <summary>
        /// Method for applying the actual platform-specific render state onto the device.
        /// </summary>
        /// <param name="state">State to be applied</param>
        protected override void ApplyConcreteState(RenderState state) {
            //Silently fail if null, should not be the case
            if(state == null) {
                return;
            }

            //If we haven't been bound, automatically do it now
            if(!state.IsBound) {
                state.BindRenderState();
            }

            switch(state.StateType) {
                case RenderStateType.BlendState:
                    BlendState bs = state as BlendState;

                    //Set the D3D10 blend state
                    _graphicsDevice.OutputMerger.BlendState = ((D3D10BlendStateImplementation) bs.Implementation).D3D10BlendState;

                    //Override the high frequency states
                    BlendFactor = bs.BlendFactor;
                    MultiSampleMask = bs.MultiSampleMask;
                    break;
                case RenderStateType.DepthStencilState:
                    DepthStencilState dss = state as DepthStencilState;

                    //Set the D3D10 depth stencil state
                    _graphicsDevice.OutputMerger.DepthStencilState = ((D3D10DepthStencilStateImplementation) dss.Implementation).D3D10DepthStencilState;

                    //Override the high frequency states
                    ReferenceStencil = dss.ReferenceStencil;
                    break;
                case RenderStateType.RasterizerState:
                    RasterizerState rs = state as RasterizerState;

                    //Set the D3D10 rasterizer state
                    _graphicsDevice.Rasterizer.State = ((D3D10RasterizerStateImplementation) rs.Implementation).D3D10RasterizerState;
                    break;
            }
        }

        /// <summary>
        /// Set the active pass, so we can reference input layouts.
        /// </summary>
        /// <param name="pass">Pass to be active</param>
        internal void OnApplyPass(D3D10EffectPass pass) {
            _currentPass = pass;
        }


        //Flushes any remaining sampler/texture states -after- pass.Apply(),
        //so they do not get overwritten by the effects API. This will
        //only set new states (anything that has been changed since
        //the last time we've flushed).
        private void FlushStates() {
            _vertexSamplers.FlushState();
            _samplers.FlushState();
            _vertexTextures.FlushState();
            _textures.FlushState();
        }

        /// <summary>
        /// Releases unmanaged and - optionally - managed resources
        /// </summary>
        /// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
        protected override void Dispose(bool disposing) {
            if(!base.IsDisposed) {
                if(disposing) {
                    //Clear references
                    Array.Clear(_currentVertexBuffers, 0, _currentVertexBufferCount);
                    Array.Clear(_vertexBindings, 0, _currentVertexBufferCount);
                    _renderTargetManager.ClearAll();

                    _tracker.ReleaseAllResources();
                    _graphicsDevice.ClearState();
                    _graphicsDevice.Dispose();
                    _factory.Dispose();
                }
                base.Dispose(disposing);
#if DEBUG
                System.Diagnostics.Debug.Write(SlimDX.ObjectTable.ReportLeaks());
#endif
            }
        }
    }
}
